import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import { Tag } from './entities/tag.entity';
import { ContentTagMapping } from './entities/content-tag-mapping.entity';
import { CreateTagDto } from './dto/create-tag.dto';
import { QueryTagDto } from './dto/query-tag.dto';
import { PaginatedResponseDto, PaginationMetaDto } from '../common/dto/pagination.dto';
import { TagResponseDto } from './dto/tag-response.dto';

@Injectable()
export class TagsService {
  constructor(
    @InjectRepository(Tag)
    private readonly tagRepository: Repository<Tag>,
    @InjectRepository(ContentTagMapping)
    private readonly mappingRepository: Repository<ContentTagMapping>,
  ) {}

  async findAll(queryDto: QueryTagDto): Promise<PaginatedResponseDto<TagResponseDto>> {
    const { name, page = 1, pageSize = 20 } = queryDto;

    // Build query
    const queryBuilder = this.tagRepository.createQueryBuilder('tag');

    // Apply name filter if provided
    if (name) {
      queryBuilder.where('tag.name LIKE :name', { name: `%${name}%` });
    }

    // Get total count for pagination
    const total = await queryBuilder.getCount();

    // Apply pagination
    queryBuilder.skip((page - 1) * pageSize).take(pageSize);

    // Execute query
    const tags = await queryBuilder.getMany();

    // Transform to response DTOs
    const tagDtos = tags.map((tag) => this.mapTagToDto(tag));

    // Create pagination metadata
    const pagination: PaginationMetaDto = {
      total,
      page,
      pageSize,
    };

    // Return response DTO
    return {
      data: tagDtos,
      pagination,
    };
  }

  // Helper method to map Tag entity to TagResponseDto
  private mapTagToDto(tag: Tag): TagResponseDto {
    const dto = new TagResponseDto();
    dto.id = tag.id;
    dto.name = tag.name;
    return dto;
  }

  async create(createTagDto: CreateTagDto): Promise<TagResponseDto> {
    const existingTag = await this.tagRepository.findOne({
      where: { name: createTagDto.name },
    });

    if (existingTag) {
      throw new ConflictException('Tag with this name already exists');
    }

    const tag = this.tagRepository.create(createTagDto);
    const savedTag = await this.tagRepository.save(tag);
    return this.mapTagToDto(savedTag);
  }

  async findOne(id: string): Promise<TagResponseDto> {
    // Validate UUID format
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    if (!uuidRegex.test(id)) {
      throw new NotFoundException(`Tag with ID "${id}" not found`);
    }

    const tag = await this.tagRepository.findOne({
      where: { id },
    });

    if (!tag) {
      throw new NotFoundException(`Tag with ID "${id}" not found`);
    }

    return this.mapTagToDto(tag);
  }

  /**
   * Find a tag by its exact name
   * @param name The exact name of the tag to find
   * @returns The tag with the specified name
   * @throws NotFoundException if no tag with the specified name exists
   */
  async findByExactName(name: string): Promise<TagResponseDto> {
    const tag = await this.tagRepository.findOne({
      where: { name },
    });

    if (!tag) {
      throw new NotFoundException(`Tag with name "${name}" not found`);
    }

    return this.mapTagToDto(tag);
  }

  async remove(id: string): Promise<void> {
    try {
      // Validate UUID format
      const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
      if (!uuidRegex.test(id)) {
        throw new NotFoundException(`Tag with ID "${id}" not found`);
      }

      // Check if tag exists
      const tag = await this.tagRepository.findOne({ where: { id } });
      if (!tag) {
        throw new NotFoundException(`Tag with ID "${id}" not found`);
      }

      // Check if tag is being used by any content
      const mappingCount = await this.mappingRepository.count({ where: { tagId: id } });
      if (mappingCount > 0) {
        throw new ConflictException('Cannot delete tag that is being used by content');
      }

      // Delete the tag
      const result = await this.tagRepository.delete(id);
      if (result.affected === 0) {
        throw new NotFoundException(`Tag with ID "${id}" not found`);
      }
    } catch (error) {
      if (error instanceof NotFoundException || error instanceof ConflictException) {
        throw error;
      }
      throw new NotFoundException(`Tag with ID "${id}" not found`);
    }
  }

  async addContentTag(contentId: string, tagId: string): Promise<ContentTagMapping> {
    try {
      const tag = await this.tagRepository.findOne({ where: { id: tagId } });
      if (!tag) {
        throw new NotFoundException(`Tag with ID "${tagId}" not found`);
      }

      const mapping = this.mappingRepository.create({
        contentId,
        tagId,
      });

      try {
        return await this.mappingRepository.save(mapping);
      } catch (error) {
        if (error.code === '23505') {
          // Unique constraint violation
          const existingMapping = await this.mappingRepository.findOne({
            where: { contentId, tagId },
          });
          return existingMapping;
        }
        throw error;
      }
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      throw new NotFoundException(`Tag with ID "${tagId}" not found`);
    }
  }

  async removeContentTag(contentId: string, tagId: string): Promise<void> {
    try {
      const tag = await this.tagRepository.findOne({ where: { id: tagId } });
      if (!tag) {
        throw new NotFoundException(`Tag with ID "${tagId}" not found`);
      }

      const result = await this.mappingRepository.delete({
        contentId,
        tagId,
      });

      if (result.affected === 0) {
        throw new NotFoundException('Content-Tag mapping not found');
      }
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      throw new NotFoundException(`Tag with ID "${tagId}" not found`);
    }
  }

  async getContentTags(contentId: string): Promise<Tag[]> {
    const mappings = await this.mappingRepository.find({
      where: { contentId },
      relations: ['tag'],
    });

    return mappings.map((mapping) => mapping.tag);
  }

  async update(id: string, updateTagDto: { name: string }): Promise<TagResponseDto> {
    try {
      // Check if tag exists
      const tag = await this.tagRepository.findOne({ where: { id } });
      if (!tag) {
        throw new NotFoundException(`Tag with ID "${id}" not found`);
      }

      // Check if name is already taken by another tag
      if (updateTagDto.name !== tag.name) {
        const existingTag = await this.tagRepository.findOne({
          where: { name: updateTagDto.name },
        });

        if (existingTag && existingTag.id !== id) {
          throw new ConflictException('Tag with this name already exists');
        }
      }

      // Update tag
      tag.name = updateTagDto.name;
      const updatedTag = await this.tagRepository.save(tag);
      return this.mapTagToDto(updatedTag);
    } catch (error) {
      if (error instanceof NotFoundException || error instanceof ConflictException) {
        throw error;
      }
      throw new NotFoundException(`Tag with ID "${id}" not found`);
    }
  }
}
